#!/usr/bin/env perl

# Generate dependencies in a form suitable for inclusion into a Makefile.
# The source filenames are provided in a file, one per line.  Directories
# to be searched for the source files and for their dependencies are provided
# in another file, one per line.  Output is written to STDOUT.
#
# For CPP type dependencies (lines beginning with #include), or for Fortran
# include dependencies, the dependency search is recursive.  Only
# dependencies that are found in the specified directories are included.
# So, for example, the standard include file stdio.h would not be included
# as a dependency unless /usr/include were one of the specified directories
# to be searched.
#
# For Fortran module USE dependencies (lines beginning with a case
# insensitive "USE", possibly preceded by whitespace) the Fortran compiler
# must be able to access the .mod file associated with the .o file that
# contains the module.  In order to correctly generate these dependencies
# the following restriction must be observed.
#
# ** All modules that are to be contained in the dependency list must be
# ** contained in one of the source files in the list provided on the command
# ** line.
#
# The reason for this restriction is that the modules have a nominal dependence
# on the .o files. If a module is being used for which the source code is not
# available (e.g., a module from a library), then adding a .o dependency for
# that module is a mistake because make will attempt to build that .o file, and
# will fail if the source code is not available.
#
# Original version: B. Eaton
#                   Climate Modelling Section, NCAR
#                   Feb 2001
#
# ChangeLog:
# -----------------------------------------------------------------------------
# Modifications to Brian Eaton's original to relax the restrictions on
# source file name matching module name and only one module per source
# file.  Also added a new "-d depfile" option which allows an additional
# file to be added to every dependence.
#
#
#   Tom Henderson
#   Global Systems Division, NOAA/OAR
#   Mar 2011
# -----------------------------------------------------------------------------
# Several updates:
#
#  - Remove limitation that modules cannot be named "procedure".
#
#  - Allow optional "::" in use statement (Fortran 2003).
#
#  - Instead of having .o files depend on other .o files directly,
#    have them depend indirectly through the .mod files. This allows
#    the compiler to have discretion over whether to update a .mod,
#    and prevents cascading recompilation when it does not.
#
#
#   Sean Santos
#   CESM Software Engineering Group, NCAR
#   Mar 2013
# -----------------------------------------------------------------------------
# More updates:
#
#  - Restore ability to recognize .mod files in the path, if there's no source
#    file that provides the same module.
#
#  - Allow "non_intrinsic" keyword (Fortran 2003).
#
#   Sean Santos
#   CESM Software Engineering Group, NCAR
#   Mar 2013
# -----------------------------------------------------------------------------
# Modifications to Santos' version needed by the NEMO ocean model:
#
#  - Handling of module use statements activated by CPP macros, i.e.:
#
#     #ifdef KEY
#       USE mod1
#     #else
#       USE mod2
#     #endif
#
#  - Handling of Fortran code inclusion through CPP "#include"
#    (NEMO's *.h90 files)
#
# When preprocessing is required (-p option) fortran files *.F and *.F90 are
# preprocessed before serching for module dependencies. Preprocessed files are
# saved in a temp subdir of the current dir which is removed at the end of execution.
# The default preprocessor command is 'cpp'.
# User can override it setting the env variable CPP before execution.
# CPP macros can be defined (-D option) or undefined (-U option).
# CPP search path can be defined (-I option).
# Defined/undefined macros and search path can be passed in the env variable
# CPPFLAGS too.
#
#   Pier Giuseppe Fogli
#   ANS, CMCC
#   Jun 2013
# -----------------------------------------------------------------------------

use Getopt::Long qw(:config bundling);
use File::Basename;
use File::Temp qw/ :POSIX /;

# Check for usage request.
@ARGV >= 2                          or usage();

# Process command line.
my $opt_w = 0;
my $obj_dir;
my $additional_file;
my $do_cpp = 0;
my $mangle_scheme = "lower";
my @cpp_def_key;
my @cpp_undef_key;
my @cpp_inc_path;
GetOptions('p' => \$do_cpp,
	'w' => \$opt_w,
	'D=s' => \@cpp_def_key,
	'U=s' => \@cpp_undef_key,
	'I=s' => \@cpp_inc_path,
	't=s' => \$obj_dir,
	'd=s' => \$additional_file,
	'm=s' => \$mangle_scheme,
) or usage();
@ARGV == 2                        or usage();  # Check that only 2 files remain
my $filepath_arg = shift()        or usage();
my $srcfile_arg = shift()         or usage();
@ARGV == 0                        or usage();  # Check that all args were processed.

# Setup CPP stuff if needed
my $cpp = "cpp";
my $red = " ";
if ($do_cpp){
    my @cpp_keys = ();
    my @cpp_path = ();
    #
    # Override default cpp from env
    $ENV{"CPP"} and $cpp = $ENV{"CPP"} ;
    $ENV{"CPPFLAGS"} and @$cpp_keys = $ENV{"CPPFLAGS"} ;
    #
    foreach $k (@cpp_def_key){
	push @$cpp_keys, "-D".$k ;
    }
    foreach $k (@cpp_undef_key){
	push @$cpp_keys, "-U".$k ;
    }
    foreach $k (@cpp_inc_path){
	push @$cpp_path, "-I".$k ;
    }
    if ($cpp =~ /gcc/){ $red = ">"; }
}

open(FILEPATH, $filepath_arg) or die "Can't open $filepath_arg: $!\n";
open(SRCFILES, $srcfile_arg) or die "Can't open $srcfile_arg: $!\n";

# Make list of paths to use when looking for files.
# Prepend "." so search starts in current directory.  This default is for
# consistency with the way GNU Make searches for dependencies.
my @file_paths = <FILEPATH>;
close(FILEPATH);
chomp @file_paths;
unshift(@file_paths,'.');
foreach $dir (@file_paths) {  # (could check that directories exist here)
    $dir =~ s!/?\s*$!!;  #!# remove / and any whitespace at end of directory name
    ($dir) = glob $dir;  # Expand tildes in path names.
}

# Make list of files containing source code.
my @src = <SRCFILES>;
close(SRCFILES);
chomp @src;

my %module_files = ();

# Attempt to parse each file for /^\s*module/ and extract module names
# for each file.
my ($f, $name, $path, $suffix, $mod);
# include NEMO's *.h90 files
my @suffixes = ('\.[fFh]90', '\.[fF]','\.F90\.in' );
foreach $f (@src) {
    ($name, $path, $suffix) = fileparse($f, @suffixes);
    # find the file in the list of directorys (in @file_paths)
    my $file_path = find_file($f);
    open(FH, $file_path)  or die "Can't open $file_path: $!\n";
    while ( <FH> ) {
	# Search for module definitions.
	if ( /^\s*MODULE\s+(\w+)\s*(\!.*)?$/i ) {
	    ($mod = $1) =~ tr/A-Z/a-z/;
            if ( defined $module_files{$mod} ) {
                die "Duplicate definitions of module $mod in $module_files{$mod} and $name: $!\n";
            }
            $module_files{$mod} = $name;
	}
    }
    close( FH );
}

# Now make a list of .mod files in the file_paths.  If a source dependency
# can't be found based on the module_files list above, then maybe a .mod
# module dependency can if the mod file is visible.
my %trumod_files = ();
my ($dir);
my ($f, $name, $path, $suffix, $mod);
# This might not be clear: we want to mangle a "\" so that it will escape
# the "." in .mod or .MOD
my @suffixes = (mangle_modfile("\\"));
foreach $dir (@file_paths) {
    # Similarly, this gets us $dir/*.mod or $dir/*.MOD
    @filenames = (glob("$dir/".mangle_modfile("*")));
    foreach $f (@filenames) {
       ($name, $path, $suffix) = fileparse($f, @suffixes);
       ($mod = $name) =~ tr/A-Z/a-z/;
       $trumod_files{$mod} = $name;
    }
}

#print STDERR "\%module_files\n";
#while ( ($k,$v) = each %module_files ) {
#    print STDERR "$k => $v\n";
#}

# Find module and include dependencies of the source files.
my ($file_path, $rmods, $rincs);
my %file_modules = ();
my %file_includes = ();
my @check_includes = ();
my %modules_used = ();

# Create a temp dir for preprocessed files if needed
my $tmp_dir;
my $tmp_file;
if ($do_cpp){
    my $tmp_nam = tmpnam();
    $tmp_dir = basename($tmp_nam);
    my $cmd = "mkdir " . $tmp_dir;
    system($cmd) == 0 or die "Failed to run command $cmd !\n";
}

foreach $f ( @src ) {

    # Find the file in the seach path (@file_paths).
    unless ($file_path = find_file($f)) {
	if ($opt_w) {print STDERR "$f not found\n";}
	next;
    }

    # Preprocess if required
    ($name, $path, $suffix) = fileparse($f, @suffixes);
    if ($do_cpp && $suffix =~ /\.F/){
        $tmp_file = catfile($tmp_dir, $name . lc($suffix));
	my $cmd = $cpp . " " . "@$cpp_path" . " " . "@$cpp_keys" . " " . $file_path . $red . $tmp_file ;
	system($cmd) == 0 or die "Failed to run command $cmd !\n";
	$file_path = $tmp_file;
    }

    # Find the module and include dependencies.
    ($rmods, $rincs) = find_dependencies( $file_path );

    # Remove redundancies (a file can contain multiple procedures that have
    # the same dependencies).
    $file_modules{$f} = rm_duplicates($rmods);
    $file_includes{$f} = rm_duplicates($rincs);

    # Make a list of all include files.
    push @check_includes, @{$file_includes{$f}};
}

# Remove temp preprocessed file
if ($do_cpp){
  my $cmd = "rm -rf " . $tmp_dir;
  system($cmd) == 0 or die "Failed to run command: $cmd !\n";
}

#print STDERR "\%file_modules\n";
#while ( ($k,$v) = each %file_modules ) {
#    print STDERR "$k => @$v\n";
#}
#print STDERR "\%file_includes\n";
#while ( ($k,$v) = each %file_includes ) {
#    print STDERR "$k => @$v\n";
#}
#print STDERR "\@check_includes\n";
#print STDERR "@check_includes\n";

# Find include file dependencies.
my %include_depends = ();
while (@check_includes) {
    $f = shift @check_includes;
    if (defined($include_depends{$f})) { next; }

    # Mark files not in path so they can be removed from the dependency list.
    unless ($file_path = find_file($f)) {
	$include_depends{$f} = -1;
	next;
    }

    # Find include file dependencies.
    ($rmods, $include_depends{$f}) = find_dependencies($file_path);

    # Add included include files to the back of the check_includes list so
    # that their dependencies can be found.
    push @check_includes, @{$include_depends{$f}};

    # Add included modules to the include_depends list.
    if ( @$rmods ) { push @{$include_depends{$f}}, @$rmods;  }
}

#print STDERR "\%include_depends\n";
#while ( ($k,$v) = each %include_depends ) {
#    print STDERR (ref $v ? "$k => @$v\n" : "$k => $v\n");
#}

# Remove include file dependencies that are not in the Filepath.
my $i, $ii;
foreach $f (keys %include_depends) {

    unless (ref $include_depends{$f}) { next; }
    $rincs = $include_depends{$f};
    unless (@$rincs) { next; }
    $ii = 0;
    $num_incs = @$rincs;
    for ($i = 0; $i < $num_incs; ++$i) {
    	if ($include_depends{$$rincs[$ii]} == -1) {
	    splice @$rincs, $ii, 1;
	    next;
	}
    ++$ii;
    }
}

# Substitute the include file dependencies into the %file_includes lists.
foreach $f (keys %file_includes) {
    my @expand_incs = ();

    # Initialize the expanded %file_includes list.
    my $i;
    unless (@{$file_includes{$f}}) { next; }
    foreach $i (@{$file_includes{$f}}) {
	push @expand_incs, $i  unless ($include_depends{$i} == -1);
    }
    unless (@expand_incs) {
	$file_includes{$f} = [];
	next;
    }

    # Expand
    for ($i = 0; $i <= $#expand_incs; ++$i) {
	push @expand_incs, @{ $include_depends{$expand_incs[$i]} };
    }

    $file_includes{$f} = rm_duplicates(\@expand_incs);
}

#print STDERR "expanded \%file_includes\n";
#while ( ($k,$v) = each %file_includes ) {
#    print STDERR "$k => @$v\n";
#}

# Print dependencies to STDOUT.

print "# Declare all module files used to build each object.\n";

foreach $f (sort keys %file_modules) {
    my $file;
    if($f =~ /\.F90\.in$/){
    	$f =~ /(.+)\.F90\.in/;
	$file = $1;
    }else{
        $f =~ /(.+)\./;
	$file = $1;
    }
    $target = $obj_dir."$file.o";
    print "$target : $f @{$file_modules{$f}} @{$file_includes{$f}} $additional_file \n";
}

print "# The following section relates each module to the corresponding file.\n";
$target = mangle_modfile("%");
print "$target : \n";
print "\t\@\:\n";

foreach $mod (sort keys %modules_used) {
    my $mod_fname = $obj_dir.mangle_modfile($mod);
    my $obj_fname = $obj_dir.$module_files{$mod}.".o";
    print "$mod_fname : $obj_fname\n";

}

#--------------------------------------------------------------------------------------

sub find_dependencies {

    # Find dependencies of input file.
    # Use'd Fortran 90 modules are returned in \@mods.
    # Files that are "#include"d by the cpp preprocessor are returned in \@incs.

    # Check for circular dependencies in \@mods.  This type of dependency
    # is a consequence of having multiple modules defined in the same file,
    # and having one of those modules depend on the other.

    my( $file ) = @_;
    my( @mods, @incs );

    open(FH, $file)  or die "Can't open $file: $!\n";

    # Construct the makefile target associated with this file.  This is used to
    # check for circular dependencies.
    my ($name, $path, $suffix, $target);
    my @suffixes = ('\.[fF]90', '\.[fF]','\.F90\.in' );
    ($name, $path, $suffix) = fileparse($file, @suffixes);
    $target = "$name.o";

    while ( <FH> ) {
	# Search for CPP include and strip filename when found.
	if ( /^#\s*include\s+[<"](.*)[>"]/ ) {
	    $include = $1;
	 }
	# Search for Fortran include dependencies.
	elsif ( /^\s*include\s+['"](.*)['"]/ ) {                   #" for emacs fontlock
	    $include = $1;
	}
	if(defined($include)){
	    if($include =~ /shr_assert.h/){
		push @mods, "$obj_dir".mangle_modfile("shr_assert_mod");
	    }
	    push @incs, $include;
	    undef $include;
	}
	# Search for module dependencies.
	elsif ( /^\s*USE(?:\s+|\s*\:\:\s*|\s*,\s*non_intrinsic\s*\:\:\s*)(\w+)/i ) {
	    # Return dependency in the form of a .mod file
	    ($module = $1) =~ tr/A-Z/a-z/;
	    if ( defined $module_files{$module} ) {
		# Check for circular dependency
		unless ("$module_files{$module}.o" eq $target) {
                    $modules_used{$module} = ();
                    push @mods, "$obj_dir".mangle_modfile($module);
		}
	    }
            # If we already have a .mod file around.
            elsif ( defined $trumod_files{$module} ) {
                push @mods, "$obj_dir".mangle_modfile($trumod_files{$module});
            }
	}
    }
    close( FH );
    return (\@mods, \@incs);
}

#--------------------------------------------------------------------------------------

sub find_file {

# Search for the specified file in the list of directories in the global
# array @file_paths.  Return the first occurance found, or the null string if
# the file is not found.

    my($file) = @_;
    my($dir, $fname);

    foreach $dir (@file_paths) {
	$fname = "$dir/$file";
	if ( -f  $fname ) { return $fname; }
    }
    return '';  # file not found
}

#--------------------------------------------------------------------------------------

sub rm_duplicates {

# Return a list with duplicates removed.

    my ($in) = @_;       # input arrary reference
    my @out = ();
    my $i;
    my %h = ();
    foreach $i (@$in) {
	$h{$i} = '';
    }
    @out = keys %h;
    return \@out;
}

#--------------------------------------------------------------------------------------

sub mangle_modfile {

# Return the name of the module file corresponding
# to a given module.

    my ($mod) = @_;
    my $fname;

    if ($mangle_scheme eq "lower") {
        ($fname = $mod) =~ tr/A-Z/a-z/;
        $fname .= ".mod";
    } elsif ($mangle_scheme eq "upper") {
        ($fname = $mod) =~ tr/a-z/A-Z/;
        $fname .= ".MOD";
    } else {
        die "Unrecognized mangle_scheme!\n";
    }

    return $fname;

}

#--------------------------------------------------------------------------------------

sub usage {
    ($ProgName = $0) =~ s!.*/!!;            # name of program
    die <<EOF
SYNOPSIS
     $ProgName [-p [-Dmacro[=val]] [-Umacro] [-Idir]] [-d depfile]
               [-m mangle_scheme] [-t dir] [-w] Filepath Srcfiles
OPTIONS
     -p
          Preprocess files (suffix .F and .F90) before  searching
	  for module dependencies. Default CPP preprocessor: cpp.
	  Set env variables CPP and/or CPPFLAGS to override.
     -D macro[=val]
          Define the CPP macro with val as its value.
	  Ignored when -p option is not active.
     -U macro
          Undefine the CPP macro.
	  Ignored when -p option is not active.
     -I dir
          Add dir to the include path for CPP.
	  Ignored when -p option is not active.
     -d depfile
          Additional file to be added to every .o dependence.
     -m mangle_scheme
          Method of mangling Fortran module names into .mod filenames.
          Allowed values are:
              lower - Filename is module_name.mod
              upper - Filename is MODULE_NAME.MOD
          The default is -m lower.
     -t dir
          Target directory.  If this option is set the .o files that are
          targets in the dependency rules have the form dir/file.o.
     -w   Print warnings to STDERR about files or dependencies not found.
ARGUMENTS
     Filepath is the name of a file containing the directories (one per
     line) to be searched for dependencies.  Srcfiles is the name of a
     file containing the names of files (one per line) for which
     dependencies will be generated.
EOF
}
